#! /usr/bin/python2.5
# This file is part of SyncStar, free software released under GPL v2.
# Google AppEngine Python 2.5 source.

class EntryEnv(object):
  """An database and cache I/O environment for a singe key--value entry.

  This is an abstract class.

  Values must be comparable, i.e. `value1 == value2' must work if value1 is
  not None, and it must return false if value2 is None.
  """

  def get_cache(self):
    """Returns the value in the cache or None iff not found.

    Returns:
      The value in the cache or None iff not found.
    """
    raise NotImplementedError

  def delete_cache(self):
    """Deletes the entry from the cache (it's OK if it wasn't there)."""
    raise NotImplementedError

  def cas_cache(self, value):
    """Puts the new value to the cache unless it's too late.
   
    Args:
      value: Any non-None value.
    Returns:
      bool indicating whether the entry was unchanged since the last put.
      If false is returned, then the cache entry is not changed.
    """
    raise NotImplementedError

  def get_db(self):
    """Returns the value in the database or None iff not found.

    Must be called within a transaction.

    Returns:
      The value in the database or None iff not found.
    """
    raise NotImplementedError

  def put_db(self, value):
    """Puts the new or updated value to the database, overwriting the old entry.

    Must be called within a transaction.
    
    Args:
      value: Any non-None value.
    """
    raise NotImplementedError

  def run_in_transaction(self, function, *args, **kwargs):
    """Runs function(*args, **kwargs) in a database transaction.

    The function might be called many times (in case of retries because of
    conflicts), and it must not have side effects.

    The semantics are of AppEngine db.run_in_transaction.

    Args:
      function: Function to run in a transaction. May be called multiple times.
      args: Arguments to pass to the function.
      kwargs: Keyword arguments to pass to the function.
    Returns:
      The return value of the last call to the function.
    """
    raise NotImplementedError


def update_cache_and_db(combiner, is_combiner_idempotent, delta, entry_env):
  """Updates the database and the cache with the specified delta.

  If an exception happens either in the transaction or in the combiner running
  in the transaction, then this function behaves safely and removes the entry
  from the cache as a precaution.

  TODO: Analyze more failure modes (who throws which exceptions).

  Args:
    combiner: Function that will be called one or more times as
      combiner(value, delta) for some values, and returns a (new_value, retval)
      pair. This is the function which decides what the new value should be
      given the old value and the delta to apply. Must not have side effects
      other than logging. Must not change the original value in any way.
    is_combiner_idempotent: bool indicating if
      combiner(combiner(value, delta)[0], delta) == combiner(value, delta) is
      always true for all values.
    delta: The new information to be be combined to the existing value. Can be
      of any type combiner accepts.
    entry_env: Instance of a subclass of EntryEnv, implementing the cache and
      the database.
  Returns:
    The second value (retval) returned by the combiner upon its last call.
  """
  mcold = entry_env.get_cache()
  if mcold is None:
    mcnew = None
    retvals = [None]
  else:
    retvals = list(combiner(mcold, delta))
    assert len(retvals) == 2
    mcnew = retvals.pop(0)
    assert mcnew is not None, 'combiner returned None as mcnew value'
  if mcold is not None and mcold == mcnew:
    return retvals[0]
  def transaction_body():
    dbold = entry_env.get_db()
    if (dbold is not None and
        ((dbold == mcnew and is_combiner_idempotent) or
         dbold == mcold)):
      dbnew = mcnew  # Never None.
    else:
      dbnew, retvals[0] = combiner(dbold, delta)
      assert dbnew is not None, 'combiner returned None as dbnew value'
    if dbnew != dbold:
      entry_env.put_db(dbnew)
    return dbnew
  do_delete = True
  try:
    dbnew = entry_env.run_in_transaction(transaction_body)
    if dbnew == mcold or entry_env.cas_cache(dbnew):
      do_delete = False
    return retvals[0]
  finally:
    if do_delete:
      entry_env.delete_cache()

